﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace TK
{
    class CustomSerializerState
    {
        internal readonly CustomSerializerSettings settings;
        internal readonly Stream stm;
        internal readonly int Version;

        readonly HashSet<object> processedObjects = new HashSet<object>();
        readonly List<ObjectHeader> objects = new List<ObjectHeader>();
        readonly Dictionary<string, UInt16> names = new Dictionary<string, UInt16>();
        readonly Dictionary<UInt16, string> namesByID = new Dictionary<UInt16, string>();
        UInt16 nextNameIndex = 0;        

        internal CustomSerializerState(Stream stm, CustomSerializerSettings settings, int version = 0)
        {
            this.settings = settings;
            this.stm = stm;
            names.Add("", nextNameIndex);
            nextNameIndex++;
            Version = version;
        }

        internal bool checkRings(object value, string fieldName) {
            if (value == null || value.GetType().IsValueType) return false;
            if(processedObjects.Contains(value)) throw new CustomSerializerException(SerializerFailType.RingDetected, value, fieldName);
            processedObjects.Add(value);
            return false;
        }

        #region readName / writeName
        internal void writeName(string value) {
            UInt16 nameIndex;
            if (!names.TryGetValue(value, out nameIndex)) {
                names.Add(value, nameIndex = nextNameIndex);
                namesByID.Add(nameIndex, value);
                nextNameIndex++;
            }
            writeUInt16(nameIndex);
        }

        internal string readName()
        {
            var id = readUInt16();
            return namesByID[id];
        }

        internal void writeTypeName(Type type)
        {
            writeName(type == null ? "" : type.AssemblyQualifiedName);
        }

        internal string readTypeName()
        {
            return readName();
        }
        #endregion

        #region writeUInt16 / readUInt16 / writeInt32 / readInt32
        internal void writeUInt16(UInt16 value, Stream toStream = null) { (toStream ?? stm).Write(BitConverter.GetBytes(value), 0, 2); }
        internal void writeInt32(Int32 value, Stream toStream = null) { (toStream ?? stm).Write(BitConverter.GetBytes(value), 0, 4); }

        internal UInt16 readUInt16(Stream fromStream = null)
        {
            var b = new byte[2];
            (fromStream ?? stm).Read(b, 0, 2);
            return BitConverter.ToUInt16(b, 0);
        }

        internal Int32 readInt32(Stream fromStream = null)
        {
            var b = new byte[4];
            (fromStream ?? stm).Read(b, 0, 4);
            return BitConverter.ToInt32(b, 0);
        }
        #endregion

        #region writeHeader / updateHeader / readHeader
        internal int writeHeader() {
            var currentObjectID = objects.Count;

            var header = new ObjectHeader(stm.Position);
            objects.Add(header);
            stm.Write(BitConverter.GetBytes(currentObjectID), 0, 4);
            return currentObjectID;
        }
        
        internal void updateHeader(int objectID, UInt16 count = 0) {
            var obj = objects[objectID];
            obj.Length = stm.Position - objects[objectID].Length;
            obj.Count = count;
        }

        internal ObjectHeader readHeader()
        {
            var pos = stm.Position;
            var id = readInt32();
            var o = objects[id];
            o.Offset = pos;
            return o;
        }

        internal ObjectHeader this[int index] {get { return objects[index]; }}
        #endregion

        #region writeHeaders / readHeaders
        internal void writeHeaders() {
            Int64 offset = stm.Position;

            var st = new MemoryStream();
            var gz = settings.CompressNames ? (Stream) new GZipStream(st, CompressionMode.Compress, true) : st;

            // write object lengths table
            for (var i = 0; i < objects.Count; i++) {
                var obj = objects[i];
                var bLength = BitConverter.GetBytes(obj.Length);
                gz.Write(bLength, 0, bLength.Length);

                var bCount = BitConverter.GetBytes(obj.Count);
                gz.Write(bCount, 0, bCount.Length);
            }

            // write names table
            foreach (var item in names) {
                writeUInt16(item.Value, gz);                 // 2
                var b = Encoding.Default.GetBytes(item.Key);
                writeUInt16((UInt16)b.Length, gz);           // 2
                gz.Write(b, 0, b.Length);                    // N
            }
            if (settings.CompressNames)
            {
                gz.Flush();
                gz.Close();
            }

            stm.Write(st.ToArray(), 0, (int) st.Length);

            // write HEADER (offset, objects count, names count)
            var bOffset = BitConverter.GetBytes(offset);
            stm.Write(bOffset, 0, bOffset.Length);                      // 8

            var bObjectCount = BitConverter.GetBytes(objects.Count);
            stm.Write(bObjectCount, 0, bObjectCount.Length);            // 4

            var bNamesCount = BitConverter.GetBytes(names.Count);
            stm.Write(bNamesCount, 0, bNamesCount.Length);              // 4
        }

        internal void readHeaders() {
            var currentOffset = stm.Position;            

            // read HEADER (offset, objects count, names count)
            var bOffsetCount = new byte[8 + 4 + 4];
            stm.Position = stm.Length - bOffsetCount.Length;
            stm.Read(bOffsetCount, 0, bOffsetCount.Length);

            var offset = BitConverter.ToInt64(bOffsetCount, 0);
            var objectCount = BitConverter.ToInt32(bOffsetCount, 8);
            var nameCount = BitConverter.ToInt32(bOffsetCount, 12);

            stm.Position = offset;
            objects.Clear();
            names.Clear();
            namesByID.Clear();

            var gz = settings.CompressNames ? (Stream) new GZipStream(stm, CompressionMode.Decompress) : stm;

            // read LENGTHs
            var b = new byte[8 + 2];
            for (var i = 0; i < objectCount; i++) {
                gz.Read(b, 0, b.Length);
                objects.Add(new ObjectHeader(BitConverter.ToInt64(b, 0)) { Count = BitConverter.ToUInt16(b, 8) });
            }

            // read NAMES
            //Trace.WriteLine("readHeaders: " + nameCount);
            for (var i = 0; i < nameCount; i++)
            {
                var nameID = readUInt16(gz);
                b = new byte[readUInt16(gz)];
                gz.Read(b, 0, b.Length);

                var s1 = Encoding.Default.GetString(b);
                names.Add(s1, nameID);
                namesByID.Add(nameID, s1);
            }            

            stm.Position = currentOffset;
        }
        #endregion

        internal void skipObject(ObjectHeader header)
        {
            stm.Seek(header.Offset + header.Length, SeekOrigin.Begin);
        }

        internal bool validToProcessing(PropInfo propinfo)
        {
            return ((!propinfo.IsProperty && settings.IncludeFields) || (propinfo.IsProperty && settings.IncludeProperties));// && (propinfo.Version == 0 || Version >= propinfo.Version);
        }

#if DEBUG
        public override string ToString()
        {
            return "Position/Length=" + stm.Position + "/" + stm.Length + ", Objects=" + objects.Count;
        }
#endif
    }

    sealed class ObjectHeader
    {
        internal Int64 Offset;
        internal Int64 Length;
        internal UInt16 Count;

        public ObjectHeader() { }
        internal ObjectHeader(Int64 length) { Length = length; }

#if DEBUG
        public override string ToString()
        {
            return "Length=" + Length + ", Count=" + Count;
        }
#endif
    }
}